<template>
    <div class="tip">
        <div class="tip_box">
            <div class="tip_section"><b>文件大小：</b>由于服务器存储空间限制，仅支持上传不超过2G的文件。</div>
            <div class="tip_section">
                <b>断点续传：</b>大文件上传到一半的时候，刷新页面，重新上传同个文件，就会从上次的进度继续。
            </div>
            <div class="tip_section"><b>秒传：</b>已经上传过的大文件，再次上传，立即返回上传完成。</div>
        </div>
    </div>
    <div class="container">
        <el-card class="card">
            <template #header>
                <div class="card_header">普通文件上传</div>
            </template>
            <input
                @change="normal.inputChange"
                :disabled="normal.isWorking"
                type="file"
                class="card_input"
                placeholder="普通文件上传"
            />
            <el-button type="primary" :loading="normal.isWorking" @click="normal.uploadFile()">上传</el-button>
            <div class="card_progress">
                上传进度：
                <div class="card_progress_box">
                    <div class="card_progress_bar" :style="{ width: normal.progress + '%' }"></div>
                </div>
                <div class="card_progress_value">{{ normal.progress }}%</div>
            </div>
            <template #footer>
                <div class="card_link">
                    文件链接：<a v-if="normal.link" :href="normal.link" target="_blank">{{ normal.link }}</a>
                </div>
            </template>
        </el-card>
        <el-card class="card">
            <template #header>
                <div class="card_header">大文件上传</div>
            </template>
            <input
                @change="big.inputChange"
                :disabled="big.isWorking"
                type="file"
                class="card_input"
                placeholder="大文件上传"
            />
            <el-button type="primary" :loading="big.isWorking" @click="big.uploadFile()">上传</el-button>
            <div class="card_progress">
                上传进度：
                <div class="card_progress_box">
                    <div class="card_progress_bar" :style="{ width: big.progress + '%' }"></div>
                </div>
                <div class="card_progress_value">{{ big.progress }}%</div>
            </div>
            <template #footer>
                <div class="card_link">
                    文件链接：<a v-if="big.link" :href="big.link" target="_blank">{{ big.link }}</a>
                </div>
            </template>
        </el-card>
    </div>
    <div class="generate">
        <div class="generate_title">本地生成测试专用大文件</div>
        文件大小：<el-input
            class="generate_input"
            v-model.number="generate.size"
            type="number"
            :min="100"
            :max="2000"
            :step="100"
        />
        M
        <el-button type="warning" class="generate_button" @click="generate.start()">生成</el-button>
    </div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { ElMessage } from "element-plus";
import api from "@/api/upload-api";
import streamSaver from "streamsaver";
import { createMD5 } from "hash-wasm";
import Concurrency from "@/utils/concurrency";

/**
 * 普通文件上传
 */
const normal = reactive({
    progress: 0,
    link: "",
    file: null as File,
    isWorking: false, // 进行中
    inputChange(e) {
        normal.progress = 0;
        normal.link = "";
        normal.file = e.target.files[0];
    },
    // 上传文件
    uploadFile() {
        if (normal.isWorking) return;
        if (!normal.file) {
            ElMessage.warning("请先选择文件");
            return;
        }
        // 文件大小限制
        if (normal.file.size > 2 * 1024 * 1024 * 1024) {
            ElMessage.warning("文件不能超过2G");
            return;
        }
        normal.isWorking = true;
        // 文件名编码（防止中文乱码）
        let fileName = encodeURIComponent(normal.file.name);
        let file = new File([normal.file], fileName, { type: normal.file.type });
        // 上传
        api.uploadFile({ file }, ({ loaded, total }) => {
            normal.progress = Math.floor((loaded / total) * 100);
        }).then((res) => {
            normal.link = res.data.fileUrl;
            normal.isWorking = false;
        });
    },
});

/**
 * 大文件上传
 */
const big = reactive({
    uploaded: 0,
    progress: 0,
    link: "",
    file: null as File,
    isWorking: false, // 进行中
    concurrency: new Concurrency(5), // 并发控制，最多5个
    inputChange(e) {
        big.progress = 0;
        big.link = "";
        big.file = e.target.files[0];
    },
    // 上传文件
    async uploadFile() {
        if (big.isWorking) return;
        if (!big.file) {
            ElMessage.warning("请先选择文件");
            return;
        }
        if (big.file.size > 2 * 1024 * 1024 * 1024) {
            ElMessage.warning("文件不能超过2G");
            return;
        }
        big.isWorking = true;
        // 获取切片文件数组
        const chunkFiles = big.getChunkFiles(big.file);
        // 获取hash值、后缀名
        const hash = await big.getFileHash(chunkFiles);
        const suffix = "." + big.file.name.split(".").pop();

        // 在服务端检查切片
        let checkRes = await api.checkChunk({ hash, suffix });
        const { fileUrl, chunkIndexList } = checkRes.data;
        // 文件已存在了，直接结束（秒传）
        if (fileUrl) {
            big.link = fileUrl;
            big.progress = 100;
            big.isWorking = false;
            return;
        }
        big.uploaded = 0;
        for (let i = 0; i < chunkFiles.length; i++) {
            // 切片已存在了，直接跳过
            if (chunkIndexList.includes(i)) {
                big.uploaded++;
                continue;
            }
            // 上传切片（加入并发控制）
            big.concurrency.execute(() => {
                return api
                    .uploadChunk({
                        hash,
                        chunkIndex: String(i),
                        chunkFile: chunkFiles[i],
                    })
                    .then(() => {
                        // 上传进度
                        big.uploaded++;
                        big.progress = Math.floor((big.uploaded / chunkFiles.length) * 100);
                        // 全部上传完成，发送合并请求
                        if (big.uploaded == chunkFiles.length) {
                            api.mergeChunk({ hash, suffix }).then(({ data }) => {
                                big.link = data.fileUrl;
                                big.isWorking = false;
                            });
                        }
                    });
            });
        }
        big.progress = Math.floor((big.uploaded / chunkFiles.length) * 100);
    },
    // 根据文件获取切片数组
    getChunkFiles(file: File) {
        let chunkFiles: Blob[] = [];
        // 默认切片大小为1M，当文件大于100M时，切片大小为1/100文件大小
        let size = 1024 * 1024;
        if (file.size > 100 * 1024 * 1024) {
            size = Math.ceil(file.size / 100);
        }
        // 切片数量
        const count = Math.ceil(file.size / size);
        // 保存切片数据
        for (let index = 0; index < count; index++) {
            chunkFiles.push(file.slice(index * size, (index + 1) * size));
        }
        return chunkFiles;
    },
    // 获取文件 hash
    async getFileHash(chunkFiles: Blob[]) {
        const md5 = await createMD5();
        for (let i = 0; i < chunkFiles.length; i++) {
            const buffer = await new Promise<ArrayBuffer>((resolve) => {
                const reader = new FileReader();
                reader.onload = (e) => resolve(e.target.result as ArrayBuffer);
                reader.readAsArrayBuffer(chunkFiles[i]);
            });
            md5.update(new Uint8Array(buffer));
        }
        return md5.digest("hex");
    },
});

/**
 * 生成大文件
 */
const generate = reactive({
    size: 100,
    chunkSize: 1024 * 1024,
    start() {
        if (!generate.size || generate.size <= 0 || generate.size > 2048) {
            ElMessage.warning("请输入大于0且不超过2048的数字，服务器只支持最多上传2G");
            return;
        }
        const fileSize = Math.floor(generate.size * 1024 * 1024); // 强制为整数
        const writer = streamSaver
            .createWriteStream(`测试专用大文件-${generate.size}M.zip`, { size: fileSize })
            .getWriter();

        let written = 0;
        const zeroChunk = new Uint8Array(generate.chunkSize);
        const writeChunk = async () => {
            while (written < fileSize) {
                const remain = fileSize - written;
                const curChunk = remain >= generate.chunkSize ? zeroChunk : new Uint8Array(remain);
                await writer.write(curChunk);
                written += curChunk.length;
            }
            await writer.close();
            ElMessage.success("测试专用大文件生成完成！");
        };
        writeChunk();
    },
});
</script>
<style scoped lang="scss" src="./upload.scss"></style>
